﻿#region Usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using Microsoft.Win32;
using WaveletStudio.Functions;

#endregion

namespace DataInterpolator.Model
{
   public class DataService : IDataService
   {
      private static readonly Random _Rand = new Random();
      private string _BotFormationTitle = "";

      private WellDatabase _Database = new WellDatabase();

      private int _EvenDataCount;
      private double _EvenDataXEnd;
      private double _EvenDataXStart;

      private int _InterpolationLevels = 3;
      private IList<InterpPair> _InterpPairs = new List<InterpPair>();

      private bool _IsRefreshing;

      private DataSet _LeftInterpolatedSet;

      private Well _LeftWell;
      private readonly List<DataSet> _MidInterpolatedSets = new List<DataSet>();
      private DataSet _RightInterpolatedSet;
      private Well _RightWell;
      private double _SamplingDistance = -1.0f;
      private string _TopFormationTitle = "";
      private double _WindowMax;
      private double _WindowMin;
      private string _ZeroReference = "KB";

      public DataService()
      {
         //DesignDataService.GenerateDataSets(100, (left, right) =>
         //{
         //   _LeftFile.RowCount = left.Count;
         //   _LeftFile.RowData = new double[_LeftFile.RowCount];
         //   _LeftFile.IndexedYValues.Clear();

         //   for (int i = 0; i < _LeftFile.RowCount; i++)
         //   {
         //      _LeftFile.RowData[i] = left[i].X;
         //      _LeftFile.IndexedYValues.Add(left[i].Y);
         //   }

         //   _RightFile.RowCount = right.Count;
         //   _RightFile.RowData = new double[_RightFile.RowCount];
         //   _RightFile.IndexedYValues.Clear();

         //   for (int i = 0; i < _RightFile.RowCount; i++)
         //   {
         //      _RightFile.RowData[i] = right[i].X;
         //      _RightFile.IndexedYValues.Add(right[i].Y);
         //   }

         //});

         _SamplingDistance = -1.0;
      }

      #region IDataService Members

      public event Action<string> StatusMessage;
      public event Action<PartialUpdateTypes> WellDataUpdated;

      public void GetInterpolatedValues(Action<DataSet, IList<DataSet>, DataSet, double, double> callback)
      {
         if (!IsReady())
            return;

         try
         {
            callback(_LeftInterpolatedSet,
                     _MidInterpolatedSets,
                     _RightInterpolatedSet,
                     _LeftWell.OutputData.YOffset,
                     _RightWell.OutputData.YOffset);
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred during interpolation", ex);
         }
      }

      public void SetInterpolationLevels(int levelCount)
      {
         if (_InterpolationLevels == levelCount)
            return;

         _InterpolationLevels = levelCount;

         try
         {
            OnWellDataUpdated(UpdateMidInterpolation());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting interpolation level", ex);
         }
      }

      public void SetInterpolationPairs(IList<InterpPair> pairs)
      {
         if (pairs == null)
            return;

         _InterpPairs = pairs;

         try
         {
            OnWellDataUpdated(UpdateMidInterpolation());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting interpolation pairs", ex);
         }
      }

      public void SetSamplingDistance(double distanceFeet)
      {
         if (_SamplingDistance == distanceFeet)
            return;

         _SamplingDistance = distanceFeet;

         try
         {
            OnWellDataUpdated(UpdateIndexedData());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting sampling distance", ex);
         }
      }

      public bool LoadDatabaseFile(string filename)
      {
         try
         {
            _Database.Load(filename);

            //Now indicate that the wells have changed
            OnFullRefresh();

            return true;
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while loading database: " + filename, ex);
         }

         return false;
      }

      public void ImportLogFiles(IList<string> fileNames)
      {
         List<LogFile> logFiles = new List<LogFile>();

         //Create a log file for each filename
         for (int i = 0; i < fileNames.Count; i++)
         {
            var log = new LogFile(fileNames[i]);

            try
            {
               log.ParseFile();
            }
            catch (Exception ex)
            {
               OnExceptionOccurred("Error occurred while parsing file: " + fileNames[i], ex);
            }

            logFiles.Add(log);
         }

         Dictionary<string, Well> wells = new Dictionary<string, Well>();

         //Now pair fils and create the corresponding Well Objects
         foreach (var log in logFiles)
         {
            Well well = null;

            if (!wells.ContainsKey(log.Well.Title))
            {
               //Create new well
               well = log.Well;

               wells[well.Title] = well;
            }
            else
            {
               well = wells[log.Well.Title];

               //Check to make sure that the data matches
               if (well.KB != log.Well.KB || well.TimeDatum != log.Well.TimeDatum)
                  continue;
            }

            //Make sure that we dont overwrite data
            if (well.Tops == null && log.Well.Tops != null)
               well.Tops = log.Well.Tops;
            else if (well.LogData == null && log.Well.LogData != null)
               well.LogData = log.Well.LogData;
         }

         //Now add or overwrite any well
         foreach (var well in wells.Values)
         {
            if (well.LogData == null || well.Tops == null)
               continue;

#if DEBUG
            //Temp for testing, if the well already exists, randomize the data
            if (_Database.TitleToID(well.Title) != Guid.Empty)
            {
               double scale = _Rand.NextDouble() * 0.02 + 0.99;

               for (int i = 1; i < well.LogData.Count; i++)
               {
                  double diff = well.LogData[i].Depth - well.LogData[i - 1].Depth;

                  well.LogData[i].Depth = well.LogData[i - 1].Depth + RandGaussian(diff, Math.Abs(diff / 4.0));
               }

               foreach (var form in well.Tops.Formations)
               {
                  form.Depth *= _Rand.NextDouble() * 0.02 + 0.99;
                  form.Time *= _Rand.NextDouble() * 0.02 + 0.99;
               }

               well.Title = well.Title + "-" + _Database.Wells.Count();
            }
#endif

            _Database.Wells[well.ID] = well;
         }

         //Now indicate that the wells have changed
         OnFullRefresh();
      }

      public void GetWellData(Action<IList<Well>> callback)
      {
         try
         {
            callback(_Database.WellList);
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while retrieving well data", ex);
         }
      }

      public string SaveDatabaseFile(bool showDialog)
      {
         try
         {
            if (!showDialog)
            {
               //Try to save with the current filename. If failed, fallback to the prompt
               if (_Database.Save())
                  return _Database.Filename;
            }

            SaveFileDialog sfd = new SaveFileDialog();
            sfd.AddExtension = true;
            sfd.DefaultExt = ".xml";
            sfd.Filter = "XML File (*.xml)|*.xml|All Files (*.*)|*.*";
            sfd.Title = "Specify The Database File";

            if (!sfd.ShowDialog().GetValueOrDefault(false))
               return null;

            _Database.Save(sfd.FileName);

            return _Database.Filename;
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while saving database", ex);
            return string.Empty;
         }
      }

      public void RemoveWells(IList<Guid> wellIDs)
      {
         foreach (var id in wellIDs)
            _Database.Wells.Remove(id);

         //Now indicate that the wells have changed
         OnFullRefresh();
      }

      public void NewDatabase()
      {
         _Database = new WellDatabase();

         //Now indicate that the wells have changed
         OnFullRefresh();
      }

      public bool SetZeroRef(string refPoint)
      {
         if (string.IsNullOrWhiteSpace(refPoint))
            refPoint = "KB";

         if (_ZeroReference == refPoint)
            return true;

         string oldRef = _ZeroReference;
         _ZeroReference = refPoint;

         try
         {
            OnWellDataUpdated(UpdateOutput());
            return true;
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting zero reference", ex);
            _ZeroReference = oldRef;
            return false;
         }
      }

      public void SetTopFormation(string topFormationTitle)
      {
         if (_TopFormationTitle == topFormationTitle)
            return;

         _TopFormationTitle = topFormationTitle;

         try
         {
            OnWellDataUpdated(UpdateAnalysis());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting top formation", ex);
         }
      }

      public void SetBotFormation(string botFormationTitle)
      {
         if (_BotFormationTitle == botFormationTitle)
            return;

         _BotFormationTitle = botFormationTitle;

         try
         {
            OnWellDataUpdated(UpdateAnalysis());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting bottom formation", ex);
         }
      }

      public bool SetWindow(double minValue, double maxValue)
      {
         if (_WindowMin == minValue && _WindowMax == maxValue)
            return true;

         double oldMin = _WindowMin;
         double oldMax = _WindowMax;

         _WindowMin = minValue;
         _WindowMax = maxValue;

         try
         {
            OnWellDataUpdated(UpdateOutput());
            return true;
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting window values", ex);
            _WindowMin = oldMin;
            _WindowMax = oldMax;
            return false;
         }
      }

      public void SetLeftWell(string wellTitle)
      {
         Well left = null;

         Guid id = _Database.TitleToID(wellTitle);

         if (_Database.Wells.ContainsKey(id))
            left = _Database.Wells[id];

         if (_LeftWell == left)
            return;

         _LeftWell = left;

         try
         {
            OnWellDataUpdated(UpdateIndexedData());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting left well", ex);
         }
      }

      public void SetRightWell(string wellTitle)
      {
         Well right = null;

         Guid id = _Database.TitleToID(wellTitle);

         if (_Database.Wells.ContainsKey(id))
            right = _Database.Wells[id];

         if (_RightWell == right)
            return;

         _RightWell = right;

         try
         {
            OnWellDataUpdated(UpdateIndexedData());
         }
         catch (Exception ex)
         {
            OnExceptionOccurred("Error occurred while setting right well", ex);
         }
      }

      public bool UnsavedChanges()
      {
         if (_Database != null)
            return _Database.UnsavedChanges;

         return false;
      }

      public void UpdateWell(Guid id, Func<Well, bool> callback)
      {
         if (_Database.Wells.ContainsKey(id))
         {
            try
            {
               if (callback(_Database.Wells[id]))
               {
                  OnWellDataUpdated(OnInputChanged());
                  _Database.UnsavedChanges = true;
               }
            }
            catch (Exception ex)
            {
               OnExceptionOccurred("Error occurred while updating well", ex);
            }
         }
      }

      public bool ExportInterpolation(ExportInterpInfo exportInfo)
      {
         if (!IsReady())
            return false;

         string dir = Path.GetDirectoryName(Path.GetFullPath(exportInfo.Filename));
         string fileName = Path.GetFileNameWithoutExtension(Path.GetFullPath(exportInfo.Filename));
         string fileExt = Path.GetExtension(Path.GetFullPath(exportInfo.Filename));

         for (int i = 0; i < _MidInterpolatedSets.Count; i++)
         {
            //Create the next filename
            string nextFilename = Path.Combine(dir, fileName + "-" + (i + 1) + fileExt);

            //Check if that filename exists already
            if (File.Exists(nextFilename))
            {
               MessageBoxResult promptResult = MessageBox.Show("One of the interpolation files, " + nextFilename + ", already exists. Would you like to overwrite the file? Select 'Cancel' to abort the export process.", "File Already Exists", MessageBoxButton.YesNoCancel);

               if (promptResult == MessageBoxResult.No)
               {
                  continue;
               }
               else if (promptResult == MessageBoxResult.Cancel)
               {
                  return false;
               }

               //Otherwise, overwrite
            }

            //Get the LAS string
            String lasString = exportInfo.GenerateLAS(i, _MidInterpolatedSets[i]);

            //Write the string to the file
            File.WriteAllText(nextFilename, lasString);
         }

         return true;
      }

      #endregion

      private bool IsReady()
      {
         if (_LeftWell == null)
            return false;

         if (_RightWell == null)
            return false;

         return true;
      }

      private void OnStatusMessage(string statusMessage)
      {
         if (StatusMessage != null)
            StatusMessage(statusMessage);
      }

      private void OnExceptionOccurred(string message, Exception ex)
      {
         OnStatusMessage(message + Environment.NewLine + ex.Message);
      }

      private void FindLeadTrailZeros(List<double> data, out int firstValid, out int lastValid)
      {
         firstValid = data.FindIndex(val => { return val != 0.0 && !double.IsNaN(val); });

         lastValid = data.FindLastIndex(val => { return val != 0.0 && !double.IsNaN(val); });
      }

      private DataSet Respace(List<double> indexedX, List<double> indexedY, IList<double> desiredX)
      {
         if (desiredX.Count == 0)
            return null;

         int firstValid = 0;
         int lastValid = indexedX.Count - 1;

         FindLeadTrailZeros(indexedY, out firstValid, out lastValid);

         IList<double> finalF = null;
         int count = lastValid - firstValid + 1;

         WaveMath.RespacePoints(indexedX.GetRange(firstValid, count),
                                indexedY.GetRange(firstValid, count),
                                desiredX,
                                ref finalF);

         return new DataSet(_EvenDataXStart, _SamplingDistance, finalF);
      }

      private DataSet CalculateInterpStep(double coeff, List<InterpPair> indexedPairs)
      {
         if (!IsReady())
            return null;

         IList<double> stretchXLeft = new List<double>();
         IList<double> stretchXRight = new List<double>();

         InterpPair pair1 = indexedPairs[0];
         InterpPair pair2 = indexedPairs[1];

         int startIndex = 0;
         int endIndex = _EvenDataCount;

         //To ensure equal number of x values and only one x value at a time, loop over possible X index values
         for (int i = startIndex; i < endIndex; i++)
         {
            double unStrectched = i;

            double pairDepthBegin = 0.0;
            double pairDepthEnd = 0.0;

            for (int j = 1; j < indexedPairs.Count; j++)
            {
               pair1 = indexedPairs[j - 1];
               pair2 = indexedPairs[j];

               pairDepthBegin = pair1.Left * (1.0 - coeff) + pair1.Right * coeff;
               pairDepthEnd = pair2.Left * (1.0 - coeff) + pair2.Right * coeff;

               if (unStrectched >= pairDepthBegin && unStrectched < pairDepthEnd)
                  break;
            }

            double midT = (unStrectched - pairDepthBegin) / (pairDepthEnd - pairDepthBegin);

            stretchXLeft.Add(midT * (pair2.Left - pair1.Left) + pair1.Left);
            stretchXRight.Add(midT * (pair2.Right - pair1.Right) + pair1.Right);
         }

         //For each desired X, interpolate to get the stretched Y
         DataSet stretchLeft = Respace(_LeftInterpolatedSet.XValues, _LeftInterpolatedSet.Values, stretchXLeft);
         DataSet stretchRight = Respace(_RightInterpolatedSet.XValues, _RightInterpolatedSet.Values, stretchXRight);

         List<double> xVals = new List<double>();
         List<double> values = new List<double>();

         //Linear interpolate the streteched Y to get the final value
         for (int i = 0; i < endIndex - startIndex; i++)
         {
            xVals.Add(i + startIndex);
            values.Add(stretchLeft.Values[i] * (1.0 - coeff) + stretchRight.Values[i] * coeff);
         }

         var set = new DataSet(_EvenDataXStart, _SamplingDistance, values);
         set.TrueOffset = coeff * (_RightWell.OutputData.YOffset - _LeftWell.OutputData.YOffset) +
                          _LeftWell.OutputData.YOffset;
         return set;
      }

      private double RandGaussian(double mean, double stdDev)
      {
         double u1 = _Rand.NextDouble(); //these are uniform(0,1) random doubles
         double u2 = _Rand.NextDouble();
         double randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2); //random normal(0,1)
         return mean + stdDev * randStdNormal; //random normal(mean,stdDev^2)
      }

      private void OnFullRefresh()
      {
         _IsRefreshing = true;

         //_WindowMax = double.MinValue;
         //_WindowMin = double.MaxValue;

         ////Reset the min and max window
         //foreach (var well in _Database.Wells.Values)
         //{
         //   for (int i = 0; i < well.LogData.Count; i++)
         //   {
         //      if (double.IsNaN(well.LogData[i].Depth))
         //         continue;

         //      _WindowMax = Math.Max(_WindowMax, well.LogData[i].Depth);
         //      _WindowMin = Math.Min(_WindowMin, well.LogData[i].Depth);
         //   }
         //}

         //Indicate the input changed for trickle down calculations
         PartialUpdateTypes update = OnInputChanged();

         if (WellDataUpdated != null)
            WellDataUpdated(PartialUpdateTypes.Full);

         _IsRefreshing = false;
      }

      private void OnWellDataUpdated(PartialUpdateTypes updateType)
      {
         if (!_IsRefreshing && WellDataUpdated != null)
            WellDataUpdated(updateType);
      }

      private PartialUpdateTypes OnInputChanged()
      {
         return UpdateExtrapolated() | PartialUpdateTypes.Input;

         if (!_IsRefreshing && WellDataUpdated != null)
            WellDataUpdated(PartialUpdateTypes.Input);
      }

      private PartialUpdateTypes UpdateExtrapolated()
      {
         foreach (var well in _Database.Wells.Values)
            well.ExtrapolatedData.Calc(well);

         //Indicate the input changed for trickle down calculations
         return UpdateOutput() | PartialUpdateTypes.Extrapolated;
      }

      private PartialUpdateTypes UpdateOutput()
      {
         foreach (var well in _Database.Wells.Values)
            well.OutputData.Calc(well, _ZeroReference, _WindowMin, _WindowMax);

         //Indicate the input changed for trickle down calculations
         return UpdateAnalysis() | UpdateIndexedData() | PartialUpdateTypes.Output;
      }

      private PartialUpdateTypes UpdateAnalysis()
      {
         foreach (var well in _Database.Wells.Values)
            well.IsoData.Calc(well.Tops, _TopFormationTitle, _BotFormationTitle);

         return PartialUpdateTypes.Analysis;
      }

      private PartialUpdateTypes UpdateIndexedData()
      {
         if (!IsReady() || _ZeroReference == "KB" || _ZeroReference == "SeismicDatum")
            return PartialUpdateTypes.None;

         double startIndexUnrounded = 0;
         double endIndexUnrounded = 0;

         try
         {
            double leftVal =
               _LeftWell.LogData.First(
                  r =>
                  {
                     return !double.IsNaN(r.Depth) && !double.IsNaN(r.DT1) &&
                            r.Depth - _LeftWell.OutputData.YOffset >= _WindowMin &&
                            r.Depth - _LeftWell.OutputData.YOffset <= _WindowMax;
                  }).Depth -
               _LeftWell.OutputData.YOffset;

            double rightVal =
               _RightWell.LogData.First(
                  r =>
                  {
                     return !double.IsNaN(r.Depth) && !double.IsNaN(r.DT1) &&
                            r.Depth - _RightWell.OutputData.YOffset >= _WindowMin &&
                            r.Depth - _RightWell.OutputData.YOffset <= _WindowMax;
                  }).Depth -
               _RightWell.OutputData.YOffset;

            //Get min from last entry since it goes from 0 depth to max
            startIndexUnrounded = Math.Min(leftVal / _SamplingDistance, rightVal / _SamplingDistance);

            leftVal =
               _LeftWell.LogData.Last(
                  r =>
                  {
                     return !double.IsNaN(r.Depth) && !double.IsNaN(r.DT1) &&
                            r.Depth - _LeftWell.OutputData.YOffset >= _WindowMin &&
                            r.Depth - _LeftWell.OutputData.YOffset <= _WindowMax;
                  }).Depth -
               _LeftWell.OutputData.YOffset;

            rightVal =
               _RightWell.LogData.Last(
                  r =>
                  {
                     return !double.IsNaN(r.Depth) && !double.IsNaN(r.DT1) &&
                            r.Depth - _RightWell.OutputData.YOffset >= _WindowMin &&
                            r.Depth - _RightWell.OutputData.YOffset <= _WindowMax;
                  }).Depth -
               _RightWell.OutputData.YOffset;

            endIndexUnrounded = Math.Max(leftVal / _SamplingDistance, rightVal / _SamplingDistance);

            if (startIndexUnrounded > endIndexUnrounded)
            {
               throw new Exception(
                  "Invalid configuration. Sampling Distance caused indexing to result in decreasing values. Check the sign on the Sampling Distance");
            }
         }
         catch (Exception ex)
         {
            throw new Exception(
               "No DT1 data points for interpolation were found with the selected Window and Zero Reference. Consider changing the Window Min and Max.");
         }

         //determin start and stop integral amounts
         int startDepthIndex = (int)Math.Ceiling(startIndexUnrounded);
            //Round so that we are guarenteed to have at least one point at each index
         int endDepthIndex = (int)Math.Floor(endIndexUnrounded);
         _EvenDataCount = endDepthIndex - startDepthIndex + 1;

         _EvenDataXStart = startDepthIndex * _SamplingDistance;
         _EvenDataXEnd = endDepthIndex * _SamplingDistance;

         List<double> indexedX = new List<double>();
         List<double> indexedY = new List<double>();
         List<double> integers = new List<double>();

         for (int i = 0; i < _LeftWell.LogData.Count; i++)
         {
            if (double.IsNaN(_LeftWell.LogData[i].Depth))
               continue;

            if (double.IsNaN(_LeftWell.LogData[i].DT1))
               continue;

            double offsetDepth = _LeftWell.LogData[i].Depth - _LeftWell.OutputData.YOffset;

            if (offsetDepth < _WindowMin || offsetDepth > _WindowMax)
               continue;

            indexedX.Add((offsetDepth - _EvenDataXStart) / _SamplingDistance);
            indexedY.Add(_LeftWell.LogData[i].DT1);
         }

         //Update the left and right interpolated sets
         for (int i = 0; i < _EvenDataCount; i++)
            integers.Add(i);

         _LeftInterpolatedSet = Respace(indexedX, indexedY, integers);
         _LeftInterpolatedSet.TrueOffset = _LeftWell.OutputData.YOffset;

         indexedX.Clear();
         indexedY.Clear();

         for (int i = 0; i < _RightWell.LogData.Count; i++)
         {
            if (double.IsNaN(_RightWell.LogData[i].Depth))
               continue;

            if (double.IsNaN(_RightWell.LogData[i].DT1))
               continue;

            double offsetDepth = _RightWell.LogData[i].Depth - _RightWell.OutputData.YOffset;

            if (offsetDepth < _WindowMin || offsetDepth > _WindowMax)
               continue;

            indexedX.Add((offsetDepth - _EvenDataXStart) / _SamplingDistance);
            indexedY.Add(_RightWell.LogData[i].DT1);
         }

         _RightInterpolatedSet = Respace(indexedX, indexedY, integers);
         _RightInterpolatedSet.TrueOffset = _RightWell.OutputData.YOffset;

         return UpdateMidInterpolation();
      }

      private PartialUpdateTypes UpdateMidInterpolation()
      {
         if (!IsReady() || _InterpolationLevels < 1 || _LeftInterpolatedSet == null || _RightInterpolatedSet == null)
            return PartialUpdateTypes.None;

         _MidInterpolatedSets.Clear();

         List<InterpPair> indexedPairs = new List<InterpPair>(_InterpPairs);

         indexedPairs.Insert(0,
                             new InterpPair
                             {
                                Left = _LeftInterpolatedSet.FirstValidIdx,
                                Right = _RightInterpolatedSet.FirstValidIdx
                             });
         indexedPairs.Add(new InterpPair
         {
            Left = _LeftInterpolatedSet.LastValidIdx,
            Right = _RightInterpolatedSet.LastValidIdx
         });

         //Check for overlapping pairs
         for (int i = 1; i < indexedPairs.Count; i++)
         {
            if (indexedPairs[i - 1].Right > indexedPairs[i].Right)
               throw new Exception("Invalid Configuration. Interpolation Pairs must not overlap");
         }
         ;

         double increment = 1.0 / (_InterpolationLevels + 1);

         for (int i = 0; i < _InterpolationLevels; i++)
            _MidInterpolatedSets.Add(CalculateInterpStep((i + 1) * increment, indexedPairs));

         return PartialUpdateTypes.Interp;
      }
   }
}
